package org.apache.ocean.name;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang.StringUtils;
import org.apache.ocean.ConditionWait;
import org.apache.ocean.GeneralTimeoutException;
import org.apache.ocean.HttpParameters;
import org.apache.ocean.HttpTimeout;
import org.apache.ocean.IndexSchema;
import org.apache.ocean.OceanXML;
import org.apache.ocean.RefWaitNotNull;
import org.apache.ocean.RestartableTimer;
import org.apache.ocean.TimeoutState;
import org.apache.ocean.XMLUtil;
import org.jdom.Element;

/**
 * 
 * @author jasonr
 */
// TODO: unit test replicating a directory using IndexCommitPoint and SnapshotDeletionPolicy
public class NameClient {
  public static Logger log = Logger.getLogger(NameClient.class.getName());
  private Set<URL> givenNameURLs;
  EventListener eventListener;
  RestartableTimer pollTimer;
  public final RefWaitNotNull<Set<URL>> nameUrlsRef = new RefWaitNotNull<Set<URL>>();
  private final RefWaitNotNull<NameInfo> nameMasterRef = new RefWaitNotNull<NameInfo>();
  public final RefWaitNotNull<IndexSchema> indexSchemaRef = new RefWaitNotNull<IndexSchema>();
  FileClient fileClient;
  public ReportedNameInfos myReportedNameInfos;
  public NameInfo myChosenMasterName;
  public String myNameID;
  
  // TODO: implement getting name urls from any name server that works
  public NameClient(String myNameID, Set<URL> givenNameURLs, EventListener eventListener) throws Exception {
    this.myNameID = myNameID;
    this.givenNameURLs = givenNameURLs;
    this.eventListener = eventListener;
    fileClient = new FileClient(this);
    pollTimer = new RestartableTimer("nameclient poll", new Poll(), 5 * 1000, 30 * 1000);
  }

  public NameClient(Set<URL> nameURLs, EventListener eventListener) throws Exception {
    this(null, nameURLs, eventListener);
  }
  
  public void shutdown() throws Exception {
  	if (pollTimer != null) pollTimer.shutdown();
  }
  
  protected class Poll implements RestartableTimer.TimeoutRunnable {
    public void run(TimeoutState timeoutState) {
      try {
      	HttpTimeout httpTimeout = new HttpTimeout(2000, 3000, 5, timeoutState);
      	// get the name urls from the name server, add to our own
      	if (nameUrlsRef.getNoWait() == null) {
      		Set<URL> nameUrls = getNameUrlsHttp(givenNameURLs.iterator().next(), httpTimeout);
      		nameUrls.addAll(givenNameURLs);
      		nameUrlsRef.set(nameUrls);
      	}
        NamesInfosGroup namesInfosGroup = getNamesInfosGroupUntilSame(httpTimeout);
        //boolean allMatching = namesInfosGroup.allMatching();
        myChosenMasterName = namesInfosGroup.getChosenMasterNameInfo();
        //log.info("myChosenMasterName: "+myChosenMasterName);
        if (myChosenMasterName == null) {
        	nameMasterRef.set(null);
          eventListener.namesDoNotMatch(namesInfosGroup);
        } else {
        	//NameInfo masterNameInfo = namesInfosGroup.getMasterNameInfo();
        	nameMasterRef.set(myChosenMasterName);
        }
        if (indexSchemaRef.getNoWait() == null) {
					IndexSchema indexSchema = getIndexSchemaHttp(getDefaultHttpTimeout(timeoutState));
					log.info("obtained indexschema");
					for (FileClient.FileEventListener fileEventListener : indexSchema.fileEventListeners) {
						fileClient.addFileEventListener(fileEventListener);
					}
					indexSchemaRef.set(indexSchema);
				}
      } catch (GeneralTimeoutException generalTimeoutException) {
        //log.log(Level.SEVERE, "", generalTimeoutException);
        eventListener.timeout();
        removeMaster();
      } catch (Throwable throwable) {
        eventListener.exception(throwable);
        removeMaster();
      }
    }
  }
  
  public static abstract class EventListener {
    public void exception(Throwable throwable) {
    	if (throwable instanceof IOException) {
    		log.log(Level.SEVERE, throwable.getMessage());
    	} else {
        log.log(Level.SEVERE, "", throwable);
    	}
    }

    public void namesDoNotMatch(NamesInfosGroup namesInfosGroup) {
    }

    public void timeout() {
      log.info("name client timeout");
    }

    public void masterNameChanged(NamesInfosGroup namesInfosGroup) {
      log.info("masterNameChanged");
    }
  }
  
  public IndexSchema getIndexSchema(TimeoutState timeoutState) throws Exception {
		return indexSchemaRef.get(timeoutState); 
	}
  
  protected NameInfo getMasterNameInfoNoWait() throws InterruptedException {
    return nameMasterRef.getNoWait();
  }

  protected NameInfo getMasterNameInfo(TimeoutState timeoutState) throws GeneralTimeoutException, InterruptedException {
    return nameMasterRef.get(timeoutState);
  }

  protected URL getMasterNameURL(TimeoutState timeoutState) throws GeneralTimeoutException, InterruptedException {
    return nameMasterRef.get(timeoutState).url;
  }
  
  public OceanXML httpRequest(OceanXML request, TimeoutState timeoutState) throws Exception {
		URL url = getMasterNameURL(timeoutState);
		HttpTimeout httpTimeout = new HttpTimeout(1000 * 2, 1000 * 5, 2, timeoutState);
		return OceanXML.request(url, request, httpTimeout);
	}
  
  public IndexSchema getIndexSchemaHttp(HttpTimeout httpTimeout) throws Exception {
		String xml = fileClient.getFile("indexschema.xml", httpTimeout).getBodyString();
		return new IndexSchema(xml);
	}
  
  public HttpTimeout getDefaultHttpTimeout(TimeoutState timeoutState) {
  	HttpTimeout httpTimeout = new HttpTimeout(1000 * 2, 1000 * 5, 2, timeoutState);
  	return httpTimeout;
  }
  
	public OceanXML httpRequest(HttpParameters parameters, TimeoutState timeoutState) throws Exception {
		URL url = getMasterNameURL(timeoutState);
		return OceanXML.request(url, parameters, getDefaultHttpTimeout(timeoutState));
	}
  
  protected ReportedNameInfos getReportedNameInfosHttp(URL url, HttpTimeout httpTimeout) throws Exception {
    OceanXML response = OceanXML.request(url, new HttpParameters("action", "getreportednameinfos"), httpTimeout);
    Element element = response.getFirstInBody();
    ReportedNameInfos reportedNameInfos = new ReportedNameInfos(element);
    return reportedNameInfos;
  }

  protected void removeMaster() {
    nameMasterRef.set(null);
  }
  
  protected Set<URL> getNameUrlsHttp(URL url, HttpTimeout httpTimeout) throws Exception {
  	HttpParameters parameters = new HttpParameters("action", "getnameurls");
  	OceanXML response = OceanXML.request(url, parameters, httpTimeout);
  	Set<URL> set = new HashSet<URL>();
  	for (Element element : XMLUtil.getChildren("nameurl", response.bodyElement)) {
  		String urlString = element.getText();
  	  if (StringUtils.isNotBlank(urlString)) {
  	  	try {
  	  	  set.add(new URL(urlString));
  	  	} catch (MalformedURLException ex) {}
  	  }
  	}
  	return set;
  }
  
  protected OceanXML getNodes(URL url, HttpTimeout httpTimeout) throws IOException, Exception {
    HttpParameters parameters = new HttpParameters("action", "getnodes");
    return OceanXML.request(url, parameters, httpTimeout);
  }

  protected NamesInfosGroup getNamesInfosGroupUntilSame(final HttpTimeout httpTimeout) throws GeneralTimeoutException, Exception {
    ConditionWait<NamesInfosGroup> conditionWait = new ConditionWait<NamesInfosGroup>(httpTimeout.timeoutState, 10, new ConditionWait.Condition() {
      public ConditionWait.Met isMet() throws Exception {
        NamesInfosGroup namesInfosGroup = getNamesInfosGroup(httpTimeout);
        if (namesInfosGroup.allMatching()) {
          return new ConditionWait.Met(namesInfosGroup);
        }
        return null;
      }
    });
    return conditionWait.returnObject();
  }

  protected NamesInfosGroup getNamesInfosGroup(HttpTimeout httpTimeout) throws Exception {
    NamesInfosGroup namesInfosGroup = new NamesInfosGroup();
    Set<URL> nameURLs = nameUrlsRef.get(httpTimeout.timeoutState);
    for (URL url : nameURLs) {
      try {
        ReportedNameInfos reportedNameInfos = getReportedNameInfosHttp(url, httpTimeout);
        //log.info("getNamesInfosGroup reportedNameInfos: " + reportedNameInfos);
        if (StringUtils.equals(reportedNameInfos.fromNameID, this.myNameID)) {
          this.myReportedNameInfos = reportedNameInfos;
        }
        namesInfosGroup.add(reportedNameInfos);
      } catch (IOException ioException) {
        // don't add it to the list
        log.log(Level.SEVERE, ioException.getMessage());
      }
    }
    return namesInfosGroup;
  }
}
